package edu.neu.cs6515.board;

import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import lombok.Getter;
import lombok.Setter;

import com.google.common.collect.ImmutableList;
import com.google.java.contract.Ensures;
import com.google.java.contract.Requires;

import edu.neu.cs6515.board.rules.AdjacentPlacementRule;
import edu.neu.cs6515.board.rules.LegalColorRule;
import edu.neu.cs6515.board.rules.LegalFirstMoveRule;
import edu.neu.cs6515.board.rules.LegalTileRule;
import edu.neu.cs6515.board.rules.NoneOverLappingRule;
import edu.neu.cs6515.board.rules.Rule;
import edu.neu.cs6515.util.Funct;
import edu.neu.cs6515.util.Pair;
import edu.neu.cs6515.xml.IngeniousColor;
import edu.neu.cs6515.xml.Placement;
import edu.neu.cs6515.xml.Score;

/**
 * Representation of the physical board of the Ingenious game (5 Layers around
 * the central hexagon)
 *
 * TEMPORAL LOGIC / ORDER OF METHOD CALLS:
 * (Using regex syntax)
 *
 * Call anytime:
 * ---------------------------------------
 * validDistance()
 * validAngle()
 * getBrokenRules()
 * getAllDefaultHexagons()
 * isFirstRound()
 * spotForLegalTilePlacementAvailable()
 * getMaxDistance()
 * getOccupiedDefaultHexagons()
 * getHexagons()
 *
 * Call in the specified order:
 * ---------------------------------------
 * validDistance()+ getHighestAngle()
 * validDistance()+ validAngle()+ getHexagonAt()
 * getBrokenRules()+ updateHexagonBoard()
 * getBrokenRules()+ scoreAMove(Placement)
 *
 * validDistance(coord1.getDistance())+
 *      validAngle(coord1.getAngle(), coord1.getDistance())+
 *          validDistance(coord2.getDistance())+
 *              validAngle(coord2.getAngle(),coord2.getDistance())+
 *                  isNeighbor(BoardCoord bc1, BoardCoord bc2)
 *
 *
 *
 */
public final class HexagonBoard {

    private static final int LAYER_FOR_DEFAULT_HEXAGONS = 5;
    private static final int NBR_OF_HEXAGON_SIDES = 6;
    public static final int ADDITIONAL_LAYERS = 3;

    private final Map<BoardCoord, Hexagon> hexagons = new LinkedHashMap<BoardCoord, Hexagon>();

    private final Set<BoardCoord> occupiedDefaultHexagons = new HashSet<BoardCoord>();

    private List<Placement> allPlacementsOnBoard = new LinkedList<Placement>();
    
    

    private static final BoardCoord DEFAULT_TOP = new BoardCoord(
            LAYER_FOR_DEFAULT_HEXAGONS, 0);
    private static final BoardCoord DEFAULT_TOP_RIGHT = new BoardCoord(
            LAYER_FOR_DEFAULT_HEXAGONS, LAYER_FOR_DEFAULT_HEXAGONS);
    private static final BoardCoord DEFAULT_BOTTOM_RIGHT = new BoardCoord(
            LAYER_FOR_DEFAULT_HEXAGONS, 10);
    private static final BoardCoord DEFAULT_BOTTOM = new BoardCoord(
            LAYER_FOR_DEFAULT_HEXAGONS, 15);
    private static final BoardCoord DEFAULT_BOTTOM_LEFT = new BoardCoord(
            LAYER_FOR_DEFAULT_HEXAGONS, 20);
    private static final BoardCoord DEFAULT_TOP_LEFT = new BoardCoord(
            LAYER_FOR_DEFAULT_HEXAGONS, 25);

    private final int maxDistance;
    private LinkedList<Rule> rules;
    
    @Getter
    private final int nbrOfPlayers;
    
    
    @Getter
    private int moveCounter = 0;

    /**
     * Initializes Board with the size as a function of the number of players
     *
     * @param nbrOfPlayers
     *            number of participating players (between 2 and 6)
     */
    @Requires("nbrOfPlayers >= 2 && nbrOfPlayers <= 6")
    public HexagonBoard(final int nbrOfPlayers) {
        this.nbrOfPlayers = nbrOfPlayers;
        maxDistance = nbrOfPlayers + ADDITIONAL_LAYERS;
        initBoard();
    }

    /**
     * Prepares Map entries and sets links to neighbors
     *
     * @param nbrOfPlayers
     */
    private void initBoard() {
        addAllHexagonsToMap();
        setAllNeighborLinks();
        setDefaultColorsInCorners();
        initializeRules();
    }
    

    /**
     * Sets the 'printed' colors in the corners of the board
     */
    private void setDefaultColorsInCorners() {
        hexagons.get(DEFAULT_TOP).setColor(IngeniousColor.BLUE);
        hexagons.get(DEFAULT_TOP_RIGHT).setColor(IngeniousColor.ORANGE);
        hexagons.get(DEFAULT_BOTTOM_RIGHT).setColor(IngeniousColor.YELLOW);
        hexagons.get(DEFAULT_BOTTOM).setColor(IngeniousColor.PURPLE);
        hexagons.get(DEFAULT_BOTTOM_LEFT).setColor(IngeniousColor.RED);
        hexagons.get(DEFAULT_TOP_LEFT).setColor(IngeniousColor.GREEN);
    }

    /**
     * Iterates over each Hexagon and sets the links to the neighbors on each
     * side
     */
    private void setAllNeighborLinks() {
        for (int distance = 0; distance <= maxDistance; distance++) {
            for (int angle = 0; angle <= getHighestAngle(distance); angle++) {
                final Hexagon hexagon = getHexagonAt(distance, angle);
                determineAndSetNeighbors(hexagon,
                        DistanceAngleDelta.produceDelta(hexagon));
            }
        }
    }

    /**
     * Creates all hexagon objects and adds them to the map
     */
    private void addAllHexagonsToMap() {
        final Hexagon center = new Hexagon(0, 0, IngeniousColor.WHITE);
        hexagons.put(new BoardCoord(0, 0), center);
        for (int layer = 1; layer <= maxDistance; layer++) {
            for (int angle = 0; angle <= getHighestAngle(layer); angle++) {
                final Hexagon newlyAdded = new Hexagon(layer, angle,
                        IngeniousColor.WHITE);
                hexagons.put(new BoardCoord(layer, angle), newlyAdded);
            }
        }
    }

    /**
     * Sets all neighbors for a hexagon
     *
     * @param hexagon
     *            hexagon whose neighbors are being set
     * @param delta
     *            delta distance and angle information for neighbors in each
     *            sides
     */
    private void determineAndSetNeighbors(final Hexagon hexagon,
            final DistanceAngleDelta delta) {
        setNeighbors(
                adjustAndGetHexagon(
                        hexagon.getDistance() + delta.getBottomDist(),
                        hexagon.getAngle() + delta.getBottomAng()),
                adjustAndGetHexagon(
                        hexagon.getDistance() + delta.getBottomLeftDist(),
                        hexagon.getAngle() + delta.getBottomLeftAng()),
                adjustAndGetHexagon(
                        hexagon.getDistance() + delta.getBottomRightDist(),
                        hexagon.getAngle() + delta.getBottomRightAng()),
                adjustAndGetHexagon(hexagon.getDistance() + delta.getTopDist(),
                        hexagon.getAngle() + delta.getTopAng()),
                adjustAndGetHexagon(
                        hexagon.getDistance() + delta.getTopLeftDist(),
                        hexagon.getAngle() + delta.getTopLeftAng()),
                adjustAndGetHexagon(
                        hexagon.getDistance() + delta.getTopRightDist(),
                        hexagon.getAngle() + delta.getTopRightAng()), hexagon);
    }

    /**
     * adjust distance and angle, return the hexagon, null if distance too big
     *
     * @param distance
     *            dist to center
     * @param angle
     *            angle of the hexagon (TOP=0, clockwise)
     * @return hexagon specified, null if the distance is outside of the board
     */
    private Hexagon adjustAndGetHexagon(final int distance, final int angle) {
        if (distance <= 0) {
            return getHexagonAt(0, 0);
        } else if (distance > maxDistance) {
            return null;
        } else if (angle > getHighestAngle(distance)) {
            return getHexagonAt(distance, 0);
        } else if (angle < 0 && distance > 0) {
            return getHexagonAt(distance, getHighestAngle(distance));
        }
        return getHexagonAt(distance, angle);
    }

    /**
     * Sets all six neighbors of a hexagon
     *
     * @param bottomNeighbor
     *            Bottom
     * @param bottomLeftNeighbor
     *            Bottom-Left
     * @param bottomRightNeighbor
     *            Bottom-Right
     * @param topNeighbor
     *            Top
     * @param topLeftNeighbor
     *            Top-Left
     * @param topRightNeighbor
     *            Top-Right
     * @param hexagon
     *            whose neighbors are being set
     */
    private void setNeighbors(final Hexagon bottomNeighbor,
            final Hexagon bottomLeftNeighbor,
            final Hexagon bottomRightNeighbor, final Hexagon topNeighbor,
            final Hexagon topLeftNeighbor, final Hexagon topRightNeighbor,
            final Hexagon hexagon) {
        hexagon.setBottom(bottomNeighbor);
        hexagon.setBottomLeft(bottomLeftNeighbor);
        hexagon.setBottomRight(bottomRightNeighbor);
        hexagon.setTop(topNeighbor);
        hexagon.setTopLeft(topLeftNeighbor);
        hexagon.setTopRight(topRightNeighbor);
    }

    /**
     * Returns the highest angle for a given distance
     *
     * @param distance
     *            distance to center
     * @return highest angle
     */
    @Requires("validDistance(distance)")
    @Ensures("result >= 0")
    public int getHighestAngle(final int distance) {
        if (distance == 0) {
            return 0;
        } else {
            return (distance * NBR_OF_HEXAGON_SIDES) - 1;
        }
    }

    /**
     * Returns the hexagon at the specified position (dist, angle)
     *
     * @param distance
     *            dist to center
     * @param angle
     *            angle of the hex
     * @return Hexagon if dist/angle are correct, otherwise: - neg distance
     *         returns center hex - too large distance returns null - for
     *         invlalid angles the angle is rounded
     */
    @Requires({ "validDistance(distance)", "validAngle(angle, distance)" })
    @Ensures({ "result != null", "result.getDistance() == distance",
            "result.getAngle() == angle",
            "getHexagons().values().contains(result)" })
    public Hexagon getHexagonAt(final int distance, final int angle) {
        return hexagons.get(new BoardCoord(distance, angle));
    }

    /**
     * validate given distance
     *
     * @param distance
     *            distance to center hexagon
     * @return true if valid
     */
    public boolean validDistance(final int distance) {
        return distance >= 0 && distance <= getMaxDistance();
    }

    /**
     * validates given angle
     *
     * @param angle
     *            angle of the coord
     * @param distance
     *            distance of the coord
     * @return true if valid
     */
    public boolean validAngle(final int angle, final int distance) {
        return angle >= 0 && angle <= getHighestAngle(distance);
    }

    /**
     * Update the colors on the board according to the placement
     *
     * @param p
     *            placement on board
     */
    @Requires("getBrokenRules(p).isEmpty()")
    @Ensures({
            "getHexagonAt(p.getBoardCoord0().getDistance()"
                    + ", p.getBoardCoord0().getAngle()).getColor() == p.getC0()",
            "getHexagonAt(p.getBoardCoord1().getDistance()"
                    + ", p.getBoardCoord1().getAngle()).getColor() == p.getC1()" })
    public void updateHexagonBoard(final Placement p) {
        hexagons.get(p.getBoardCoord0()).setColor(p.getC0());
        hexagons.get(p.getBoardCoord1()).setColor(p.getC1());
        moveCounter++;

        final BoardCoord coord0 = p.getBoardCoord0();
        final BoardCoord coord1 = p.getBoardCoord1();
        for (final BoardCoord defaultHex : HexagonBoard.getAllDefaultHexagons()) {
            final boolean isNeighbor = isNeighbor(coord0, defaultHex)
                    || isNeighbor(coord1, defaultHex);
            if (isNeighbor) {
                this.getOccupiedDefaultHexagons().add(defaultHex);
            }
        }
        allPlacementsOnBoard.add(p);
    }
    
    /**
     * Update the colors on the board according to the list of placements
     *
     * @param p
     *            placements on board
     */
    public void updateHexagonBoard(final List<Placement> p) {
        for (Placement placement : p) {
            updateHexagonBoard(placement);
        }
    }

    /**
     * Add all rules that need to be checked to the collection
     */
    private void initializeRules() {
        rules = new LinkedList<Rule>();
        rules.add(new LegalTileRule(this));
        rules.add(new NoneOverLappingRule(this));
        rules.add(new LegalColorRule(this));
        rules.add(new AdjacentPlacementRule(this));
        rules.add(new LegalFirstMoveRule(this));
    }

    /**
     * @param p
     *            placement to validate
     * @return list of size 0 (no rules broken) or a list of size 1, containing
     *         the first broken rule
     */
    @Requires("p != null")
    @Ensures("result.size() == 0 || result.size() == 1")
    public List<Rule> getBrokenRules(final Placement p) {
        final List<Rule> brokenRules = new LinkedList<Rule>();
        for (final Rule rule : rules) {
            if (!rule.validate(p)) {
                brokenRules.add(rule);
                return brokenRules;
            }
        }
        return brokenRules;
    }

    /**
     * Determine if coord1 is a neighbor of coord2
     *
     * @param coord1
     *            Coordinates of hex1 on board
     * @param coord2
     *            Coordinates of hex2 on board
     * @return true if hex1 is a neighbor of hex2
     */
    @Requires({ "coord1 != null", "coord2 !=null",
            "validDistance(coord1.getDistance())",
            " validAngle(coord1.getAngle(), coord1.getDistance())",
            "validDistance(coord2.getDistance())",
            " validAngle(coord2.getAngle(),coord2.getDistance())" })
    public boolean isNeighbor(final BoardCoord coord1, final BoardCoord coord2) {
        final Hexagon hex1 = this.getHexagonAt(coord1.getDistance(),
                coord1.getAngle());
        boolean isNeighbor = false;
        for (Funct<Hexagon, Hexagon> funct : HexagonDirection
                .getAllDirections()) {
            isNeighbor |= safeNeighborCheck(funct.exec(hex1), coord2);
        }
        return isNeighbor;
    }

    /**
     * Compares dist and angle of two hexagons
     *
     * @param neighbor
     *            possible neighbor1
     * @param coord2
     *            possible neighbor2-coordinates
     * @return true if dist and ang are equal
     */
    private boolean safeNeighborCheck(final Hexagon neighbor,
            final BoardCoord coord2) {
        if (neighbor != null) {
            return (neighbor.getAngle() == coord2.getAngle())
                    && (neighbor.getDistance() == coord2.getDistance());
        }
        return false;
    }

    /**
     * return the default printed hexagons
     *
     * @return list of coordinates of default printed hexagons
     */
    @Ensures({ "result.size() == 6" })
    public static List<BoardCoord> getAllDefaultHexagons() {
        return ImmutableList.of(DEFAULT_TOP, DEFAULT_TOP_RIGHT,
                DEFAULT_BOTTOM_RIGHT, DEFAULT_BOTTOM, DEFAULT_BOTTOM_LEFT,
                DEFAULT_TOP_LEFT);
    }

    /**
     * @param p
     *            placement to be scored
     * @return Score object for the given placement
     */
    @Requires({"p != null" , "getBrokenRules(p).isEmpty()"})
    @Ensures({"result != null", "result.getYellow() > 0 || result.getRed() > 0 || "
            + "result.getPurple() > 0 || result.getOrange() > 0 || "
            + "result.getGreen() > 0 || result.getBlue() > 0" })
    public Score scoreAMove(final Placement p) {
        return new ScoreCalc(this).scoreAMove(p);
    }

    /**
     * Scores a move and returns the sum of the new score and the old score
     *
     * @param p
     *            new placement by player
     * @param playersScore
     *            old Score of Player before this placement
     * @return Sum of the previous score and the newly made points
     */
    @Requires({"getBrokenRules(p).isEmpty()", "playersScore != null" })
    @Ensures({"result != null", "result.getYellow() > 0 || result.getRed() > 0 || "
            + "result.getPurple() > 0 || result.getOrange() > 0 || "
            + "result.getGreen() > 0 || result.getBlue() > 0" })
    public Score scoreAMove(final Placement p, final Score playersScore) {
        Score scoreOfMove = scoreAMove(p);
        return Score.sum(ImmutableList.of(playersScore, scoreOfMove));
    }

    /**
     * check if any players have not take the first turn yet
     *
     * @return true if at least one player have not take first turn
     */
    public boolean isFirstRound() {
        return (moveCounter < this.nbrOfPlayers);
    }

    /**
     *
     * @return true if the board allows a placement to be placed (two adjacent
     *         white tiles)
     */
    public boolean spotForLegalTilePlacementAvailable() {
        for (Entry<BoardCoord, Hexagon> hexEntry : hexagons.entrySet()) {
            if (hexEntry.getValue().getColor().equals(IngeniousColor.WHITE)
                    && oneWhiteNeighborHexagon(hexEntry.getValue())) {
                return true;
            }
        }
        return false;
    }

    /**
     * @param hexagon
     *            hex whose neighbors should be checked
     * @return true if at least one of the neighbors of the hexagon is white
     */
    private boolean oneWhiteNeighborHexagon(final Hexagon hexagon) {
        for (Funct<Hexagon, Hexagon> funct : HexagonDirection
                .getAllDirections()) {
            Hexagon neighbor = funct.exec(hexagon);
            if (neighbor != null
                    && neighbor.getColor().equals(IngeniousColor.WHITE)) {
                return true;
            }
        }
        return false;
    }

    /**
     * @return the maxmum distance from the center of all hexagons on board
     *         (center distance = 0)
     */
    @Ensures("result >= 0")
    public int getMaxDistance() {
        return maxDistance;
    }

    /**
     * @return the set of all occupied default Hexagons
     */
    @Ensures("result.size() >= 0")
    public Set<BoardCoord> getOccupiedDefaultHexagons() {
        return occupiedDefaultHexagons;
    }

    /**
     * @return board's hexagons
     */
    @Ensures("result.size() >= 0")
    public Map<BoardCoord, Hexagon> getHexagons() {
        return hexagons;
    }

    /**
     * @return List with pairs of legal coordinates for placements
     */
    @Ensures("result != null")
    public List<Pair<BoardCoord>> calculateLegalOpennings() {
        List<Pair<BoardCoord>> legalOpennings = new LinkedList<Pair<BoardCoord>>();
        for (Entry<BoardCoord, Hexagon> entry : hexagons.entrySet()) {
            Hexagon hex = entry.getValue();
            for (Funct<Hexagon, Hexagon> funct : HexagonDirection
                    .getAllDirections()) {
                Hexagon neighbor = funct.exec(hex);
                if (neighbor != null
                        && isValidOpenning(hex.getAngle(), hex.getDistance(),
                                neighbor.getAngle(), neighbor.getDistance())) {
                    legalOpennings.add(createNewPair(entry.getKey(), neighbor));
                }
            }
        }
        return legalOpennings;
    }

    /**
     * @param hexCoord
     *            coords of first Hexagon of placement
     * @param neighbor
     *            neighbor of first hexagon
     * @return new Coord Pair that consists of the coordinates of the entry and
     *         its neighbor
     */
    private Pair<BoardCoord> createNewPair(final BoardCoord hexCoord,
            final Hexagon neighbor) {
        Pair<BoardCoord> newPair = new Pair<BoardCoord>(hexCoord,
                new BoardCoord(neighbor.getDistance(), neighbor.getAngle()));
        return newPair;
    }

    /**
     * @param hexAngle
     *            angle of first hex
     * @param hexDistance
     *            distance (to center) of first hex
     * @param neighbourAngle
     *            angle of neighbor of first hex
     * @param neighbourDistance
     *            distance (to center) of neighbor of first hex
     * @return true if the coordinates of the placement to not break any of the
     *         rules
     */
    private boolean isValidOpenning(final int hexAngle, final int hexDistance,
            final int neighbourAngle, final int neighbourDistance) {
        return getBrokenRules(
                new Placement(IngeniousColor.BLUE, IngeniousColor.BLUE,
                        hexDistance, hexAngle, neighbourDistance,
                        neighbourAngle)).isEmpty();
    }

    public List<Placement> getAllPlacementsOnBoard(){
        return allPlacementsOnBoard;
    }
}
